package heap;

import java.io.*;
import javax.naming.InvalidNameException;

import global.*;
import chainexception.*;

public class HeapFile {
	private int firstPID = -1;
	private Directory dir = null;
	private String fileName;


	/* Heapfile constructor with argument of heap file name to create the heap file */
	public HeapFile(String fname) throws ChainException, IOException{
		this.fileName = fname;
		if(global.Minibase.DiskManager != null){

			PageId fileHeaderPageId = global.Minibase.DiskManager.get_file_entry(fname); /* first check whether we already have this file entry */

			if(fileHeaderPageId == null){ /* if we don't have it, just create a page directory header for the file and add entry to the database */
				PageId pageDirectoryPageId = new PageId();
				Page pageDirectoryPage = new Page();

				pageDirectoryPageId = Minibase.BufferManager.newPage(pageDirectoryPage, 1);

				if(pageDirectoryPageId != null){
					firstPID = pageDirectoryPageId.pid;
					//System.out.println("first PID is : "+ firstPID);

					/* If this directory page is a blank slate. Then format it. */
					dir = new Directory(pageDirectoryPage);   
					dir.formatDirectory();
					Minibase.BufferManager.unpinPage(pageDirectoryPageId, true);

					/* Add new directory entry. */

					global.Minibase.DiskManager.add_file_entry(fname, new PageId(firstPID));

				}else{
					throw new ChainException(null, "Call to newPage for page directory failed.");
				}
			}else /* if we have the data entry in the database */
				firstPID = fileHeaderPageId.pid;


		}else{
			throw new ChainException(null, "DB object not created.");
		}
	}



	/* Insert a new record into the file and returns its RID */
	public RID insertRecord(byte[] recordData) throws InvalidSlotNumberException, SpaceNotAvailableException, HFException, HFBufMgrException, HFDiskMgrException, IOException, ChainException{
		if(recordData.length >= HeapFilePage.MAX_TUPSIZE){
			throw new SpaceNotAvailableException(null);
		}

		if(recordData.length == 0){
			throw new ChainException(null, "Can't insert empty tuples.");
		}

		RID rid = new RID();
		PageId hfPageId = getAvailableHFPageId(recordData.length);
		Page page = new Page();

		try {
			Minibase.BufferManager.pinPage(hfPageId, page, false);

			/* Create HFPage from page. */
			HeapFilePage hfPage = new HeapFilePage(page);
			rid = hfPage.insertRecord(recordData);

			/* Update directory entry corresponding to hfPage with new available space. */
			updateHFPageAvailableSpace(hfPageId, hfPage.available_space());

			Minibase.BufferManager.unpinPage(hfPageId, true);
		} catch (Exception exception) {
			throw new ChainException(exception, "Unable to insert record.");
		}

		return rid;
	}


	/* Update an existing HFpage with new space size */
	private void updateHFPageAvailableSpace(PageId hfPageId, int availableSpace) throws ChainException{
		int curPgDirPID = firstPID;

		// Start search loop.
		while(curPgDirPID != GlobalConst.INVALID_PAGEID){

			Page curPage = new Page();
			PageId curPageId = new PageId(curPgDirPID);

			try{
				Minibase.BufferManager.pinPage(curPageId, curPage, false);
			}catch(Exception exception){
				throw new ChainException(exception, "Unable to pin directory page.");
			}

			Directory curDirPage = new Directory(curPage);

			if(curDirPage.updateSpace(hfPageId, availableSpace)){
				// Space updated.

				Minibase.BufferManager.unpinPage(curPageId, true);

				return;
			}

			// Next directory.
			curPgDirPID = (curDirPage.getNext()).getPID();

			Minibase.BufferManager.unpinPage(curPageId, true);
		}
	}


	/* get the Tuple by giving record id */
	public Tuple getRecord(RID rid) throws InvalidSlotNumberException, HFException, HFDiskMgrException, HFBufMgrException, Exception{
		int curPgDirPID = firstPID;
		PageId curDirectoryPageId = new PageId(curPgDirPID);

		Page curPage = new Page();
		boolean exists = false;

		Directory curDirPage = null;
		Entry dirEntry = null;

		while(!exists && curDirectoryPageId.pid!= GlobalConst.INVALID_PAGEID){ /* find the Tuple in every heap file page */
			try{
				Minibase.BufferManager.pinPage(curDirectoryPageId, curPage, false);
			}
			catch(Exception e){
				throw new HFBufMgrException(e,"Pin Page Failed");
			}
			curDirPage = new Directory(curPage);

			/* try to find the Tuple in curent directory page */
			for(int i=0; i<curDirPage.getTotalEntries(); i++)
			{
				dirEntry = curDirPage.getEntry(i);
				PageId tmp = new PageId(dirEntry.getPID());
				if (tmp.pid == rid.pageno.pid)
				{
					exists = true;
					break;
				}
			}

			if(!exists){ /* if the Tuple is not in curent directory page */
				Minibase.BufferManager.unpinPage(curDirectoryPageId, false);
				PageId tmp1 = new PageId(curDirPage.getNext().getPID()); /* get next directory page and set as curent one */
				curDirectoryPageId.pid = tmp1.pid;
			}
		}

		if(!exists){ /* if not found */
			return null;
		}

		HeapFilePage hfDataPage = new HeapFilePage();
		Minibase.BufferManager.pinPage(new PageId(dirEntry.getPID()), hfDataPage, false);
		Tuple this_tuple = hfDataPage.getRecord(rid);
		Minibase.BufferManager.unpinPage(new PageId(dirEntry.getPID()), false);
		Minibase.BufferManager.unpinPage(curDirectoryPageId, false);
		return this_tuple;
	}


	/* get the number of records in the heap file */
	public int getRecCnt() throws InvalidNameException, IOException, ChainException{
		int count = 0;
		int curPgDirPID = firstPID;

		// Start search loop.
		while(curPgDirPID != GlobalConst.INVALID_PAGEID){
			// Pin page with PID curPgDirPID.
			Page curPage = new Page();
			PageId curPageId = new PageId(curPgDirPID);

			Minibase.BufferManager.pinPage(curPageId, curPage, false);


			Directory curDirPage = new Directory(curPage);

			//System.out.println("In getCnt(), number of total entry is: " + curDirPage.getTotalEntries()+ "and curPgDirPID:" + curPgDirPID);

			for(int entryIndex=0; entryIndex<curDirPage.getTotalEntries()-1; entryIndex++){
				Entry pageDirectoryEntry = curDirPage.getEntry(entryIndex);
				HeapFilePage hfPage = new HeapFilePage();

				if(pageDirectoryEntry.getPID() != GlobalConst.INVALID_PAGEID){
					try{
						Minibase.BufferManager.pinPage(new PageId(pageDirectoryEntry.getPID()), hfPage, false);

						// Read the HFPage to get the number of records.
						for(int slotNumber = 0; slotNumber < hfPage.getSlotCnt(); slotNumber++){
							if(hfPage.getSlotOffset(slotNumber) != 0){
								count++;
							}
						}

						// Unpin HFPage
						Minibase.BufferManager.unpinPage(new PageId(pageDirectoryEntry.getPID()), false);
					}catch(Exception exception){
						throw new HFBufMgrException(exception, "Unable to pin/unpin heapfile page.");
					}
				}
			}

			// Next directory.
			curPgDirPID = curDirPage.getNext().getPID();


			Minibase.BufferManager.unpinPage(curPageId, true);

		}

		return count;
	}


	/* update the record in rid with tuple 'newTuple', the original tuple exist then return true */
	public boolean updateRecord(RID rid, Tuple newTuple)throws InvalidSlotNumberException, InvalidUpdateException, HFException, HFDiskMgrException, HFBufMgrException, Exception
	{
		PageId curDirectoryPageId = new PageId(firstPID);
		Page curPage = new Page();

		Directory curDirPage = null;
		boolean exists = false;
		Entry dirEntry = null;

		while(!exists && curDirectoryPageId.pid!= GlobalConst.INVALID_PAGEID){ /* try to find the tuple using rid in every heap file page */

			Minibase.BufferManager.pinPage(curDirectoryPageId, curPage, false);

			curDirPage = new Directory(curPage);

			for(int i=0; i<curDirPage.getTotalEntries(); i++){

				dirEntry = curDirPage.getEntry(i);
				PageId tmp = new PageId(dirEntry.getPID());
				if (tmp.pid == rid.pageno.pid)
				{
					exists = true;
					break;
				}
			}

			if(!exists){
				Minibase.BufferManager.unpinPage(curDirectoryPageId, false);
				PageId tmp1 = new PageId(curDirPage.getNext().getPID()); /* start searching in next directory page */
				curDirectoryPageId.pid = tmp1.pid;
			}
		}

		if(!exists){ /* if the tuple is not found */
			return false;
		}

		HeapFilePage hfDataPage = new HeapFilePage();
		Minibase.BufferManager.pinPage(new PageId(dirEntry.getPID()), hfDataPage, false);
		Tuple data = hfDataPage.getRecord(rid);

		//System.out.println("lenth of new tuple: " + newTuple.getLength() + " hahahha ");


		/* after finding where is the tuple, start updating it with the new one */
		if(data.getLength() != newTuple.getLength()) /* the length is same as the new tuple we want to replace */
			throw new InvalidUpdateException(null, "length of new tuple does not match old tuple");

		data.tupleCopy(newTuple); /* just simply copy the data of new tuple into the original space */


		Minibase.BufferManager.unpinPage(new PageId(dirEntry.getPID()), true);
		Minibase.BufferManager.unpinPage(curDirectoryPageId, false);


		return true;
	}


	/* delete the record with recordID equals to 'rid' */
	public boolean deleteRecord (RID rid) throws InvalidSlotNumberException, InvalidUpdateException, HFException, HFBufMgrException, HFDiskMgrException,Exception
	{

		PageId curDirectoryPageId = new PageId(firstPID);
		Page curPage = new Page();

		Directory curDirPage = null;
		boolean exists = false;
		Entry dirEntry = null;

		while(!exists && curDirectoryPageId.pid != GlobalConst.INVALID_PAGEID){ /* same as above, first find the tuple with recordID in all heapfile pages */
			try{
				Minibase.BufferManager.pinPage(curDirectoryPageId, curPage, false);
			}
			catch(Exception e){
				throw new HFBufMgrException(e,"Pin Page Failed in deleteRecord");
			}
			curDirPage = new Directory(curPage);

			for(int i=0; i<curDirPage.getTotalEntries(); i++){
				dirEntry = curDirPage.getEntry(i);
				PageId tmp = new PageId(dirEntry.getPID());
				if (tmp.pid == rid.pageno.pid)
				{
					exists = true;
					break;
				}
			}

			if(!exists){
				try{
					Minibase.BufferManager.unpinPage(curDirectoryPageId, false);
				}
				catch(Exception ex){
					throw new HFBufMgrException(ex, "Unpin Page Failed in deleteRecord");
				}
				PageId tmp1 = new PageId(curDirPage.getNext().getPID());
				curDirectoryPageId.pid = tmp1.pid;

			}

		}
		if(!exists){ /* if the tuple we want to delete is not exist, return false */
			return false;
		}

		/* we should delete the tuple(record) in this heap file page we found */
		HFPage hfDataPage = new HFPage();
		try{
			Minibase.BufferManager.pinPage(new PageId(dirEntry.getPID()), hfDataPage, false);
		}
		catch(Exception e){
			try{
				Minibase.BufferManager.unpinPage(curDirectoryPageId, false);
			}
			catch(Exception ex){
				throw new HFBufMgrException(ex, "Failed while unpinning page in deleteRecord");
			}
			throw new HFBufMgrException(e, "Failed pinning page in deleteRecord");
		}
		hfDataPage.deleteRecord(rid);

		try{
			Minibase.BufferManager.unpinPage(new PageId(dirEntry.getPID()), true);
		}
		catch(Exception ex){
			throw new HFBufMgrException(ex, "Failed while unpinning page in deleteRecord");
		}


		/* after deleting the tuple, if the page became an empty page, then free the page */
		if(hfDataPage.firstRecord() == null){
			try{
				Minibase.BufferManager.freePage(rid.pageno);
			}
			catch(Exception ex){
				throw new HFBufMgrException(ex, "Failed while unpinning page in deleteRecord");
			}
		}
		else{
			try{
				Minibase.BufferManager.unpinPage(curDirectoryPageId, true);
			}
			catch(Exception ex){
				throw new HFBufMgrException(ex,"Unpin page failed in deleteRecord");
			}
		}
		return true;
	}


	/* delete the heap file */
	public void deleteFile() throws IOException, ChainException
	{
		PageId curDirectoryPageId = new PageId(firstPID);
		Page curPage = new Page();

		Directory curDirPage = null;
		boolean exists = false;
		Entry dirEntry = null;
		PageId nextDirPageId = new PageId();

		while(!exists && curDirectoryPageId.pid != GlobalConst.INVALID_PAGEID){

			Minibase.BufferManager.pinPage(curDirectoryPageId, curPage, false);

			curDirPage = new Directory(curPage);

			for(int i=0; i < curDirPage.getTotalEntries(); i++){ /* go through every page and try to free each one */
				dirEntry = curDirPage.getEntry(i);

				/* try to free the page */
				if(dirEntry.getPID() != GlobalConst.INVALID_PAGEID && dirEntry.getType() != Entry.DIRECTORYPAGE_ENTRY){

					Minibase.BufferManager.freePage(new PageId(dirEntry.getPID()));

				}
			}

			PageId tmp1 = new PageId(curDirPage.getNext().getPID());
			nextDirPageId.pid = tmp1.pid;

			Minibase.BufferManager.unpinPage(curDirectoryPageId, false);

			Minibase.BufferManager.freePage(curDirectoryPageId);

			curDirectoryPageId = nextDirPageId;
		}

		global.Minibase.DiskManager.delete_file_entry(fileName);

	}



	/* Given the length of record, create the heap directory pages and return the pageId of available heap file page */
	private PageId getAvailableHFPageId(int recordLength) throws ChainException{
		PageId wantedHFPageId = null;
		int curPID = firstPID;

		/* start the search loop */
		while(true){

			Page curPage = new Page();
			PageId curPageId = new PageId(curPID);

			try{ /* Pin page with PID curPgDirPID. */
				Minibase.BufferManager.pinPage(curPageId, curPage, false);
			}catch(Exception exception){
				throw new ChainException(exception, "Unable to pin curent directory page.");
			}

			Directory curDirPage = new Directory(curPage);


			if((wantedHFPageId = curDirPage.getSpace(recordLength)) != null){ /* try to get the available heap file pageId in curent directory page*/

				Minibase.BufferManager.unpinPage(curPageId, true);
				return wantedHFPageId;

			}else{ /* if cannot find in curent directory, go to the next one */

				int oldPID = curPID;
				curPID = (curDirPage.getNext()).getPID();


				if(curPID == GlobalConst.INVALID_PAGEID){

					Page newPage = new Page();
					PageId newPageId = new PageId();

					try{
						newPageId = Minibase.BufferManager.newPage(newPage, 1);

						if(newPageId != null){

							Directory newDirPage = new Directory(newPage);

							newDirPage.formatDirectory();

							curDirPage.setNext(new Entry(newPageId.pid, 0, Entry.DIRECTORYPAGE_ENTRY));

							Minibase.BufferManager.unpinPage(newPageId, true);
							Minibase.BufferManager.unpinPage(new PageId(oldPID), true);

							curPID = newPageId.pid;
						}else{
							throw new ChainException(null, "Unable to allocate new page.");
						}
					}catch(Exception exception){
						throw new ChainException(exception, "Unable to create new directory page.");
					}

				}else{
					Minibase.BufferManager.unpinPage(new PageId(oldPID), true);
				}
			}
		}
	}



	/* get first pageID */
	public int getPageDirectoryPID(){
		return firstPID;
	}


	/*create a new heap scan object */
	public HeapScan openScan() throws IOException, ChainException{
		HeapScan scan = new HeapScan(this);
		return scan;
	}


}